package remoteprocedurecall;

import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.List;

import membership.SimpleDBMembership;

import session.SessionData;

public class RPCClient {

	public static Integer callNum = new Integer(0);
	public static final String OP_PING = "ping";
	public static final String OP_PONG = "pong";
	public static final String OP_GET = "get";
	public static final String OP_PUT = "put";
	public static final String OP_VE = "ve"; // Version error
	public static final String OP_SNF = "snf"; //Session not found
	public static final String OP_PVE = "pve"; //Put version error
	
	public static final String RPC_SEP = "___";
	
	static final int CALL_ID_INDEX = 0;
	static final int OP_CODE_INDEX = 1;
	static final int DATA_INDEX = 2;
	
	static final int WritesToSend = 4;
	static final int WritesToAccept = 2;

	/**
	 * ssmGet - performs SSM get
	 * 
	 * @param sessionId
	 *            - session of interest
	 * @param version
	 *            - desired version
	 * @param destList
	 *            - list of destinations to send to
	 * @return
	 * @throws IOException
	 */
	public static DatagramPacket ssmGet(String sessionId, Integer version,
			List<RPCDestination> destList) throws IOException {
		Integer callId=null;
		synchronized(callNum){
			callId = callNum;
			callNum++;
		}
		// set up request and send packets
		byte[] outBuf = generateGet(callId, sessionId, version);
		DatagramSocket rpcSocket = new DatagramSocket();
		for (RPCDestination dest : destList) {
			DatagramPacket sendPkt = new DatagramPacket(outBuf, outBuf.length,
					InetAddress.getByName(dest.destAddress), dest.destPort);
			rpcSocket.send(sendPkt);
		}
		// set up responder
		byte[] inBuf = new byte[1024];
		DatagramPacket recvPkt = new DatagramPacket(inBuf, inBuf.length);
		try {
			do {
				recvPkt.setLength(inBuf.length);
				rpcSocket.receive(recvPkt);
			} while (!containsCorrectCallIdANDNotError(callId, inBuf));
		} catch (InterruptedIOException iioe) {
			// timeout
			recvPkt = null;
		} catch (IOException ioe) {
			// other error
		}
		rpcSocket.close();
		return recvPkt;
	}

	/**
	 * ssmPut - perform SSM put
	 * 
	 * @param sessionId
	 * @param version
	 * @param destList
	 * @return
	 * @throws IOException
	 */
	public static List<RPCDestination> ssmPut(SessionData session, List<RPCDestination> destList) throws IOException {
		Integer callId=null;
		synchronized(callNum){
			callId = callNum;
			callNum++;
		}
		// set up request and send packets
		List<RPCDestination> pcktList = new ArrayList<RPCDestination>();
		byte[] outBuf = generatePut(callId, session);
		DatagramSocket rpcSocket = new DatagramSocket();
		for (RPCDestination dest : destList) {
			//System.out.println("sending to " + InetAddress.getByName(dest.destAddress) + " port " + dest.destPort );
			DatagramPacket sendPkt = new DatagramPacket(outBuf, outBuf.length,
					InetAddress.getByName(dest.destAddress), dest.destPort);
			rpcSocket.send(sendPkt);
		}
		// accept WritesToAccept packets
		byte[] inBuf = new byte[1024];
		//TODO: This should probably not timeout??????
		rpcSocket.setSoTimeout(1000); // Set timeout to 5 seconds
		int tries = 0;
		while (pcktList.size() < WritesToAccept && tries<WritesToSend) {
			tries++;
			try {
				DatagramPacket recvPkt = new DatagramPacket(inBuf, inBuf.length);
				recvPkt.setLength(inBuf.length);
				rpcSocket.receive(recvPkt);
				//System.out.println("Packet added?"+new String(recvPkt.getData()));
				if (containsCorrectCallIdANDNotError(callId, inBuf)) {
					//System.out.println("Packet added?"+new String(recvPkt.getData()));
					pcktList.add(new RPCDestination(recvPkt.getAddress().getHostAddress(),recvPkt.getPort()));
				}
			} catch (InterruptedIOException iioe) {
				// timeout
				// recvPkt = null;
			} catch (IOException ioe) {
				// other error
			}
		}
		rpcSocket.close();
		return pcktList;
	}

	private static byte[] generatePut(Integer callId, SessionData session) {
		return (new String(String.valueOf(callId) + RPC_SEP + OP_PUT + RPC_SEP
				+ session.sessionDataToString())).getBytes();
	}

	/**
	 * generateGet - creates a get message
	 * 
	 * @param callId
	 * @param sessionId
	 * @param sessionVersion
	 * @return
	 */
	private static byte[] generateGet(Integer callId, String sessionId,
			Integer sessionVersion) {
		return (new String(String.valueOf(callId) + RPC_SEP + OP_GET + RPC_SEP
				+ sessionId + RPC_SEP + String.valueOf(sessionVersion))).getBytes();
	}

	/**
	 * containsCorrectCallId - checks if datagram data starts with the given callId
	 * @param callId
	 * @param bytes
	 * @return
	 */
	private static boolean containsCorrectCallId(Integer callId, byte[] bytes) {
		String s = new String(bytes);
		return s.startsWith(String.valueOf(callId));
	}
	
	private static boolean containsCorrectCallIdANDNotError(Integer callId,
			byte[] bytes) {
		//check if its not an error code
		String reply = new String(bytes);
		String[] dataBlocks = reply.split(RPCClient.RPC_SEP);
		String opCode = dataBlocks[OP_CODE_INDEX];
		
		if(opCode.equals(OP_VE)){
			System.out.println("Version error for get");
			return false;
		}
		else if(opCode.equals(OP_PVE)){
			System.out.println("version error for put");
			return false;
		}
		//checkk if its the right version
		String s = new String(bytes);
		return s.startsWith(String.valueOf(callId));
	}
	
	public static SessionData serverPacketToSessionData(DatagramPacket myPacket){
		
		String reply = new String(myPacket.getData());
		String[] dataBlocks = reply.split(RPCClient.RPC_SEP);
		String opCode = dataBlocks[OP_CODE_INDEX];
		String data = dataBlocks[DATA_INDEX];
		
		//If message received back had get code then get was successful
		if(opCode.equals(OP_GET)){
			return new SessionData(data);
		}		
		return null;
	}
	
	/**
	 * Create a list of RPCDestination objects by getting the address and port from each packet.
	 * 
	 * @param packets
	 * @return List<RPCDestination> destinations
	 * @throws IOException 
	 */
	public static List<RPCDestination> serverPacketsToRPCDestinationList(SessionData mySession) throws IOException{
		// get list of potential destinations from group membership code
		List<RPCDestination> destList = SimpleDBMembership.getInstance().getNServers(WritesToSend);
		//send to destinations, get packets back
		List<RPCDestination> packets = RPCClient.ssmPut(mySession, destList);
		return packets;
	}

}
